package framework.aop.intercept;

import framework.aop.aspect.InchJoinPoint;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * a class that contains store combination of point cut and aspect methods that need cut in
 *
 * recursion method proceed() implement the main idea of Aop
 */
public class InchMethodInvocation implements InchJoinPoint  {
    //proxy
    private Object proxy;
    //point cut method
    private Method method;
    //point cut instance;
    private Object target;
    //point cut class
    private Class<?> targetClass;
    //arguments for point cut
    private Object[] arguments;
    //where store the attributes
    private Map<String,Object> userAttributes ;
    //all the advises related to this point cut
    private List<Object> interceptorsAndDynamicMethodMatchers;

    // the count of the advise list
    private int currentInterceptorIndex = -1;

    /**
     * Constructor
     * @param proxy the proxy class
     * @param method the point cut method
     * @param target the point cut class instance (eg: QueryService.class.getConstructor.newInstance())
     * @param targetClass the point cut declared class(eg: QueryService.class)
     * @param arguments args
     * @param interceptorsAndDynamicMethodMatchers list of advises that signed to this point cut
     */
    public InchMethodInvocation(Object proxy, Method method, Object target, Class<?> targetClass,
                                Object[] arguments, List<Object> interceptorsAndDynamicMethodMatchers) {
        this.proxy = proxy;
        this.method = method;
        this.target = target;
        this.targetClass = targetClass;
        this.arguments = arguments;
        this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
    }

    /**
     * main method to run this function
     *
     * this is a recursion method!!
     *
     * this method execute advise, but if the advise is after the cut point
     * , the advise will call this method proceed() again to
     * let other advise execute and run the point cut.
     *
     *
     * @return return value(point cut method return value)
     * @throws Throwable
     */
    public Object proceed() throws Throwable {
        //if no advice left then run log progress
        if(this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1){
            return this.method.invoke(this.target,this.arguments);
        }
        // get next advice
        Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
        //if there is more do other advice first
        if(interceptorOrInterceptionAdvice instanceof InchMethodInterceptor) {
            InchMethodInterceptor methodInterceptor = (InchMethodInterceptor) interceptorOrInterceptionAdvice;
            //send self as a parameter to implement recurrence
            return methodInterceptor.invoke(this);
        } else {
            //if the object in list is not an advise then skip this and run other advise/point cut in this list
            return proceed();
        }
    }

    @Override
    public Method getMethod() {
        return this.method;
    }

    @Override
    public Object[] getArguments() {
        return this.arguments;
    }

    @Override
    public Object getThis() {
        return this.target;
    }

    /**
     * set the user attribute for this point cut
     * @param key the key of the attribute
     * @param value the value of the key / if the value is null then remove the attribute
     */
    @Override
    public void setUserAttribute(String key, Object value) {
        if(value != null) {
            //if not attribute before initial it
            if(this.userAttributes == null) {
                this.userAttributes = new HashMap<String, Object>();
            }
            this.userAttributes.put(key, value);
        }else {
            if(this.userAttributes != null) {
                this.userAttributes.remove(key);
            }
        }
    }

    /**
     * get a user attribute for a point cut
     * @param key the key for the attribute
     * @return value if found/ null if not
     */
    @Override
    public Object getUserAttribute(String key) {
        return this.userAttributes != null ? this.userAttributes.get(key) : null;
    }
}
